﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

namespace RZEngine2D
{
    public interface IGameState
    {
        string Name { get; set; }

        void OnEnter(string prevState);
        void OnExit(string newState);

        void OnOverride(string newState);
        void OnResume(string prevState);

        // the main per-frame things
        void Input();
        void Update(float fDT);
        void Draw();

    }

    public sealed class rzStateMachine
    {
        public ContentManager content;
        public static string g_invalidState = "invalid";

        // singleton fun stuff
        private static readonly rzStateMachine s_instance = new rzStateMachine();
        public static rzStateMachine Instance
        {
            get { return s_instance; }
        }

        // private enums, structs, and typedefs
        private enum GameStateCmdType
        {
            Push,
            Change,
            Pop,
            PopAll,
            First
        }

        private struct GameStateCmd
        {
            public GameStateCmdType cmd;
            public string stateName;
            public float delay;
        }

        // Private class data
        private List<IGameState> stateStack = new List<IGameState>();
        private Dictionary<string, IGameState> registeredGameStates = new Dictionary<string, IGameState>();
        private Queue<GameStateCmd> cmdQueue = new Queue<GameStateCmd>();

        private float currentTime = 0.0f;

        private void SendCmd(GameStateCmdType cmdType, string stateName, float delay)
        {
            //  Make it all lowercase to avoid capitalization errors
            stateName = stateName.ToLower();

            GameStateCmd cmd;
            cmd.cmd = cmdType;
            cmd.stateName = stateName;
            cmd.delay = delay;

            cmdQueue.Enqueue(cmd);
        }

        public void Shutdown()
        {
            for (int i = stateStack.Count - 1; i >= 0; --i)
            {
                stateStack[i].OnExit(g_invalidState);
            }
        }

        // Register a state object and associate it with a string identifier
        public bool RegisterState(string stateName, IGameState state)
        {
            //  Make name lowercase to avoid capitalization errors
            stateName = stateName.ToLower();

            // Check to see if state was already registered
            IGameState value;
            if (registeredGameStates.TryGetValue(stateName, out value))
            {
                // If the string ID already exists in the state map, return an error
                return false;
            }

            // Assign the object pointer to the ID location in the map
            registeredGameStates[stateName] = state;
            state.Name = stateName;

            // If this is the first state registered, put it on the stack as the first state
            if (registeredGameStates.Count == 1)
            {
                stateStack.Add(state);
                SendCmd(GameStateCmdType.First, "", 0);
            }

            return true;
        }

        // Checks if the current state will change
        // on the next update cycle
        public bool IsStateChangePending
        {
            get { return (cmdQueue.Count == 0) ? false : true; }
        }

        // Returns the current state
        public string CurrentStateName
        {
            get 
            {
                if (stateStack.Count == 0)
                    return g_invalidState;

                return stateStack[stateStack.Count - 1].Name;
            }
        }

        // Get the state object based on the string ID
        public IGameState GetState(string stateName)
        {
            // Make it lowercase to avoid capitalization errors
            stateName = stateName.ToLower();

            if (registeredGameStates.ContainsKey(stateName))
                return registeredGameStates[stateName];
            else
                throw new KeyNotFoundException("State not found: " + stateName);
        }

        // Get the state object on top of the current state stack
        public IGameState CurrentState
        {
            get { return stateStack[stateStack.Count - 1]; }
        }

        // Returns the size of the state stack
        public int StateStackSize
        {
            get { return stateStack.Count; }
        }

        // Passing shouldFlush = true will override any previous state changing
        // commands that may be pending.  Otherwise, state commands
        // will queue and be executed in the order of the calls made.

        // Changes the current state on the next update cycle.
        //
        //  NOTE: Swaps the passed in state with the current one on the top of the stack
        public void ChangeState(string stateName)
        {
            ChangeState(stateName, 0.0f, false);
        }

        public void ChangeState(string stateName, float delay, bool shouldFlush)
        {
            // Clear the queue
            if (shouldFlush)
            {
                cmdQueue.Clear();
            }

            // Push a "change state" command onto the queue
            SendCmd(GameStateCmdType.Change, stateName, currentTime + delay);
        }

        // Pushes a new state on top of the existing one on the next update cycle.
        public void PushState(string stateName)
        {
            PushState(stateName, 0.0f, false);
        }

        public void PushState(string stateName, float delay, bool shouldFlush)
        {
            // Clear the queue
            if (shouldFlush)
            {
                cmdQueue.Clear();
            }

            // Push a "push state" command onto the queue
            SendCmd(GameStateCmdType.Push, stateName, currentTime + delay);
        }

        // Pops off the current state or states to reveal a stored state
        // underneath.  You may not pop off the last state
        public void PopState()
        {
            PopState(0, 0.0f, false);
        }

        // NumStates to pop is EXTRA states to pop.
        //  It will pop off the current state REGARDLESS
        public void PopState(int numExtraStatesToPop, float delay, bool shouldFlush)
        {
            // Clear the queue
            if (shouldFlush)
            {
                cmdQueue.Clear();
            }

            // Push a "pop state" command onto the queue
            SendCmd(GameStateCmdType.Pop, "", currentTime + delay);

            //  Pop more states if needed
            while (numExtraStatesToPop > 0)
            {
                // Only the first state command can have a non-zero value
                // for delay.
                SendCmd(GameStateCmdType.Pop, "", 0.0f);
                numExtraStatesToPop--;
            }
        }

        // Pops all but the last state.
        public void PopAllStates()
        {
            PopAllStates(0.0f, false);
        }

        public void PopAllStates(float delay, bool shouldFlush)
        {
            // Clear the queue
            if (shouldFlush)
            {
                cmdQueue.Clear();
            }

            // Push a "pop all states" command onto the queue
            SendCmd(GameStateCmdType.PopAll, "", currentTime + delay);
        }

        public void ProcessCmdQueue()
        {
            // Empty our command queue, consisting of commands to either
            // push new states onto the stack, pop states off the stack, or
            // to switch the states on the top of the stack.  In each case
            // we transmit the new state to the old one, and vice-versa.
            GameStateCmd cmd;
            while ((cmdQueue.Count > 0) && (cmdQueue.Peek().delay <= currentTime))
            {
                string previousStateName = CurrentStateName;

                cmd = cmdQueue.Dequeue();

                switch (cmd.cmd)
                {
                    case GameStateCmdType.Push:
                        {
                            if (previousStateName != cmd.stateName)
                            {
                                CurrentState.OnOverride(cmd.stateName);

                                stateStack.Add(GetState(cmd.stateName));

                                CurrentState.OnEnter(previousStateName);
                            }
                        }
                        break;

                    case GameStateCmdType.Pop:
                        {
                            if (stateStack.Count > 1)
                            {
                                CurrentState.OnExit(stateStack[stateStack.Count - 2].Name);

                                stateStack.RemoveAt(stateStack.Count - 1);

                                CurrentState.OnResume(previousStateName);
                            }
                        }
                        break;

                    case GameStateCmdType.PopAll:
                        {
                            while (stateStack.Count > 1)
                            {
                                CurrentState.OnExit(stateStack[stateStack.Count - 2].Name);

                                stateStack.RemoveAt(stateStack.Count - 1);

                                CurrentState.OnResume(previousStateName);
                            }
                        }
                        break;

                    case GameStateCmdType.Change:
                        {
                            //if (previousStateName != cmd.stateName)
                            {
                                CurrentState.OnExit(cmd.stateName);

                                stateStack.RemoveAt(stateStack.Count - 1);
                                stateStack.Add(GetState(cmd.stateName));

                                CurrentState.OnEnter(previousStateName);
                            }
                        }
                        break;

                    case GameStateCmdType.First:
                        {
                            CurrentState.OnEnter(g_invalidState);
                        }
                        break;
                }
            }
        }

        // Updates the state machine internal mechanism.  This function is called
        // once by the main update loop and should not be called by anyone else.
        public void Update(float fDT)
        {
            // Update the total run time
            currentTime += fDT;

            // Check input only for the top
            CurrentState.Input();

            // After all state transitions are finished, do the current state stack updates
            // NOTE: Updates from TOP to BOTTOM
            for (int i = stateStack.Count - 1; i >= 0; i--)
                stateStack[i].Update(fDT);

            ProcessCmdQueue();

        }

        public void Draw()
        {
            // After all state transitions are finished, do the current state stack updates
            // NOTE: Draws from BOTTOM to TOP
            for (int i = 0; i < stateStack.Count; i++)
                stateStack[i].Draw();
        }
    }
}
